/*
 * BlackLightning.BlackLightning
 * Andy Tidball
 * 
 * Project: Black Lightning
 * Copyright: GNU General Public License
 */

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Runtime;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;
using System.Security.Policy;
using System.Threading;
using System.Windows.Forms;
using Microsoft.Win32;
using BlackLightning.PluginLib;
using BlackLightning.PluginLib.LoginClients;
using BlackLightning.PluginLib.Modules;
using BlackLightning.Properties;
using BLLib;
using BLLib.Configuration;
using BLLib.Net;
using BLLib.Windows.Forms;
using BLLib.Xml;

// for various Permission classes
using System.Data.Odbc;
using System.Data.SqlClient;
using System.Drawing.Printing;
using System.Net;
using System.Net.Sockets;

namespace BlackLightning {
	/// <summary>
	/// Represents the BlackLightning application itself, and contains the program entry point.
	/// </summary>
	internal class BlackLightning {
		// constants
		public  const string BlackLightningOptionSetName = "Black Lightning"; // the name of the option set containing Black Lightning's options
		public  const string PluginsOptionSetName        = "Plugins";         // the name of the option set containing all the modules
		public  const string LoginClientsOptionSetName   = "Login Clients";   // the name of the option set containing all the login clients
		private const string BlackLightningFolder        = "Black Lightning"; // the name to use for folders containing Black Lightning info
		private const string AutoLoginClientPlugin       = "PluginLib";       // the name of the plugin that contains the AutoLogin login client
		private const string AutoLoginClientType         = "AutoLogin";       // the name of the auto login login client
		private const string BLSGEPlugin                 = "PluginLib";       // the name of the plugin that contains BLSGE
		private const string BLSGEType                   = "BLSGE";           // the name of the BLSGE login client
		private const string OptionsFileExtension        = ".options";        // the extension for options files

		// constants - option value indices
		public const int GlobalOptionValueIndex    = 2; // the option value index for the machine global value of an option
		public const int UserOptionValueIndex      = 1; // the option value index for the Windows user-specific value of an option
		public const int CharacterOptionValueIndex = 0; // the option value index for the user's character-specific value of an option

		// constants - image indices
		public const int IconIndex      = 0;
		public const int FilesIconIndex = 1;
		public const int FileIconIndex  = 2;
		public const int GearIconIndex  = 3;
		public const int KeysIconIndex  = 4;
		public const int KeyIconIndex   = 5;

		// constants - options
		private const string LauncherPathOptionName               = "Launcher Path";
		private const string LauncherPathOptionDefault            = @"C:\Program Files\SIMU\Launcher.exe";
		private const string LauncherPathOptionCategory           = "System Integration";
		private const string LauncherPathOptionDescription        = "The full path and filename of the Simutronics Launcher on your system.";
		private const string DefaultLoginClientOptionName         = "Default Login Client";
		private const string DefaultLoginClientOptionDefault      = "PluginLib.BLSGE";
		private const string DefaultLoginClientOptionCategory     = "Login Settings";
		private const string DefaultLoginClientOptionDescription  = "The login client to use by default, specified in the form '<plugin>.<name>' without the quotes.  'PluginLib.BLSGE' is the default value.";
		private const string NextToolOptionName                   = "Next Tool";
		private const string NextToolOptionDefault                = "";
		private const string NextToolOptionCategory               = "Tool Integration";
		private const string NextToolOptionDescription            = "The path of the tool to execute next (i.e. PsiNet).";
		private const string RunNextToolOptionName                = "Run Next Tool";
		private const bool   RunNextToolOptionDefault             = false;
		private const string RunNextToolOptionCategory            = "Tool Integration";
		private const string RunNextToolOptionDescription         = "Whether or not to run the program specified in Next Tool rather than executing the Launcher.";
		private const string LaunchFromSGEOptionName              = "Launch from SGE";
		private const bool   LaunchFromSGEOptionDefault           = false;
		private const string LaunchFromSGEOptionCategory          = "System Integration";
		private const string LaunchFromSGEOptionDescription       = "Whether or not to setup SGE to run Black Lightning instead of the Launcher.";
		private const string BackupSGELaunchOptionName            = "Backup SGE Launch";
		private const string LaunchFromSALOptionName              = "Launch from Windows";
		private const bool   LaunchFromSALOptionDefault           = false;
		private const string LaunchFromSALOptionCategory          = "System Integration";
		private const string LaunchFromSALOptionDescription       = "Whether or not to setup Windows to run Black Lightning instead of the Launcher.  Turning this option on will also cause most browsers (though not IE) to execute Black Lightning instead of the Launcher.";
		private const string BackupSALLaunchOptionName            = "Backup SAL Launch";

		// constants - that can't be const
		private static readonly Type LauncherPathOptionTypeEditor = typeof(LauncherPathTypeEditor);
		private static readonly Type NextToolOptionTypeEditor     = typeof(NextToolPathTypeEditor);

		// static member data
		private static string               _ApplicationPath;       // the path containing BlackLightning's executable
		private static string               _PreferencesPath;       // the path to BlackLightning's application data folder
		private static string               _ApplicationExecutable; // the name of our executable file
		private static List<BlackLightning> _Instances;             // a list of BlackLightning instances currently running
		private static Mutex                _InstancesMutex;        // a mutex controlling access to Instances
		private static PermissionSet        _DefaultPermissions;    // the permissions allowed to all external functionality

		/// <summary>
		/// Static constructor.
		/// </summary>
		static BlackLightning() {
			// when I fetch the UserAppDataPath, it makes it for me.. which would be fine but it goes a little too far
			// the path ends up being .../Username/Application Settings/Company Name/App Name/App Version/
			// this is NOT the directory I want.. I merely want to know where the Application Settings directory is
			// so... we need to delete the one it gave us and move up a few and make our own..
			string UserPath = Application.UserAppDataPath;
			Directory.Delete(UserPath);                                     // delete the App Version directory
			UserPath = Directory.GetParent(UserPath).FullName;              // move up a level
			Directory.Delete(UserPath);                                     // delete the App Name directory
			UserPath = Directory.GetParent(UserPath).FullName;              // move up a level
			if (Directory.GetFiles(UserPath).Length == 0) {                 // delete the Company Name directory
				Directory.Delete(UserPath);                                 //   (unless somebody else has files in it)
			}
			UserPath = Directory.GetParent(UserPath).FullName;              // move up a level
			UserPath += Path.DirectorySeparatorChar + BlackLightningFolder; // move into the folder we DO want
			Directory.CreateDirectory(UserPath);                            // create it
			UserPath += Path.DirectorySeparatorChar;                        // tack on the trailing slash we want

			// store the appropriate paths for later use
			_ApplicationPath = Application.StartupPath + Path.DirectorySeparatorChar;
			_PreferencesPath = UserPath;

			// find the name of our executable
			string AppName = Application.ExecutablePath;                 // best we can get to start is the whole path
			string[] Split = AppName.Split(Path.DirectorySeparatorChar); // split that into folders
			AppName        = Split[Split.Length-1];                      // save the last thing, just the filename
			_ApplicationExecutable = AppName.Replace(".EXE",".exe");

			// create our default permissions
			_DefaultPermissions = new PermissionSet(PermissionState.None);
			_DefaultPermissions.AddPermission(new PrintingPermission(PrintingPermissionLevel.SafePrinting));
			_DefaultPermissions.AddPermission(new ReflectionPermission(ReflectionPermissionFlag.MemberAccess));
			_DefaultPermissions.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
			_DefaultPermissions.AddPermission(new SqlClientPermission(PermissionState.Unrestricted));
			_DefaultPermissions.AddPermission(new OdbcPermission(PermissionState.Unrestricted));
			_DefaultPermissions.AddPermission(new UIPermission(UIPermissionWindow.AllWindows, UIPermissionClipboard.AllClipboard));
			// grr, doesn't seem to be a way to provide read only access to everything
			//_DefaultPermissions.AddPermission(new EnvironmentPermission(EnvironmentPermissionAccess.Read, null));
			//_DefaultPermissions.AddPermission(new RegistryPermission(RegistryPermissionAccess.Read, null));

			// create other member data
			_Instances      = new List<BlackLightning>();
			_InstancesMutex = new Mutex();
		}

		/// <summary>
		/// Gets the path to the executable's location.  Includes a trailing slash.
		/// </summary>
		public static string ApplicationPath {
			get {
				return _ApplicationPath;
			}
		}

		/// <summary>
		/// Gets the path to Black Lightning's preferences storage.  Includes a trailing slash.
		/// </summary>
		public static string PreferencesPath {
			get {
				return _PreferencesPath;
			}
		}

		/// <summary>
		/// Gets the name of our executable file.
		/// </summary>
		public static string ApplicationExecutable {
			get {
				return _ApplicationExecutable;
			}
		}
		/// <summary>
		/// Gets the default permissions allowed to all external functionality.
		/// </summary>
		public static PermissionSet DefaultPermissions {
			get {
				return _DefaultPermissions;
			}
		}

		/// <summary>
		/// Main entry point of the application.
		/// </summary>
		/// <param name="Args">Command-line arguments passed in.</param>
		[STAThread]
		public static void Main(string[] Args) {
			try {
				SingletonApplication.ApplicationInstanceStart += CreateInstance;
				SingletonApplication.Run(Args);
			} catch (Exception x) {
				new ThreadExceptionDialog(x).ShowDialog();
				SingletonApplication.Exit();
			}
		}

		/// <summary>
		/// Creates a new security policy using the given permissions (which should be based on BlackLightning.DefaultPermissions).
		/// </summary>
		/// <param name="Permissions">The permissions to include in the policy.</param>
		/// <returns>The created security policy.</returns>
		public static PolicyLevel CreateSecurityPolicy(PermissionSet Permissions) {
			// create a code group that gives all code execution and read access to our directory
			// execution is necessary for the app domain to even run code, such as creating a *Proxy object
			// Read and PathDiscovery are necessary for finding and reading the external assembly containing the plugin's code
			PermissionSet PS = new PermissionSet(PermissionState.None);
			PS.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
			PS.AddPermission(new FileIOPermission(FileIOPermissionAccess.Read,          BlackLightning.ApplicationPath));
			PS.AddPermission(new FileIOPermission(FileIOPermissionAccess.PathDiscovery, BlackLightning.ApplicationPath));
			UnionCodeGroup BaseCodeGroup = new UnionCodeGroup(new AllMembershipCondition(), new PolicyStatement(PS));

			// create another code group that grants the given permissions to any code from our application directory
			// this allows the plugin's code itself to do the things it needs to do to work
//			UnionCodeGroup PluginCodeGroup = new UnionCodeGroup(new ApplicationDirectoryMembershipCondition(), new PolicyStatement(Permissions));
			UnionCodeGroup PluginCodeGroup = new UnionCodeGroup(new AllMembershipCondition(), new PolicyStatement(Permissions));

			// create a policy level that grants no permissions at the top level code group
			PolicyLevel PL = PolicyLevel.CreateAppDomainLevel();
//			PL.RootCodeGroup.PolicyStatement = new PolicyStatement(new PermissionSet(PermissionState.None));

			// add our two code groups beneath the root
			PL.RootCodeGroup.AddChild(BaseCodeGroup);
			PL.RootCodeGroup.AddChild(PluginCodeGroup);

			// return the policy
			return PL;
		}

		/// <summary>
		/// Checks to see if plugins written for the given version of Black Lightning are compatible with this version.
		/// </summary>
		/// <param name="Version">The version of Black Lightning to check for compatibility with this version.</param>
		/// <returns>True if the given version is compatble with this version, false if it is not.</returns>
		public static bool IsVersionCompatible(Version Version) {
			// if the given version is all zeroes, bypass the compatibility check
			if (Version.Major == 0 && Version.Minor == 0 && Version.Build == 0 && Version.Revision == 0) {
				return true;
			}

			// grab the earliest version that's compatible with this one
			Version EarliestCompatibleVersion = new Version(Resources.EarliestCompatibleVersion);
			Version ThisVersion               = new Version(Application.ProductVersion);

			// if BL's version is all zeroes, bypass the compatibility check
			if (ThisVersion.Major == 0 && ThisVersion.Minor == 0 && ThisVersion.Build == 0 && ThisVersion.Revision == 0) {
				return true;
			}

			// make sure that the given version is between the earliest compatible version and our current version
			if (Version < EarliestCompatibleVersion) {
				return false;
			} else if (Version > ThisVersion) {
				return false;
			} else {
				return true;
			}
		}

		/// <summary>
		/// Imports the SetForegroundWindow API from user32.dll so that we can use it.
		/// </summary>
		/// <param name="Hwnd">The handle of the window to bring to the foreground.</param>
		[DllImport("user32.dll", EntryPoint="SetForegroundWindow", CallingConvention=CallingConvention.StdCall, CharSet=CharSet.Unicode, SetLastError=true)]
		public static extern void SetForegroundWindow(IntPtr Hwnd);

		/// <summary>
		/// Imports the WritePrivateProfileString API from kernel32.dll so that we can use it.
		/// </summary>
		/// <param name="Section">The section of the INI file to write a string to.</param>
		/// <param name="Key">The key within the given section that we're writing the value of.</param>
		/// <param name="Value">The value to associate with the given key.</param>
		/// <param name="Filename">The filename of the INI file to write the value to.</param>
		/// <returns>True on success, false on failure.</returns>
		[DllImport("kernel32.dll", EntryPoint="WritePrivateProfileString", CallingConvention=CallingConvention.StdCall, CharSet=CharSet.Unicode, SetLastError=true)]
		public static extern bool WritePrivateProfileString(string Section, string Key, string Value, string Filename);

		/// <summary>
		/// Creates a new instance of BlackLightning and runs it.
		/// </summary>
		/// <param name="Args">Command-line parameters to pass to the new instance.</param>
		private static void CreateInstance(object Sender, ApplicationInstanceStartEventArgs e) {
			BlackLightning BL = new BlackLightning();
			_InstancesMutex.WaitOne();
			_Instances.Add(BL);
			_InstancesMutex.ReleaseMutex();
			BL.Run(e.Args);
		}

		/// <summary>
		/// Removes the given instance of BlackLightning from the list of running instances.
		/// </summary>
		/// <param name="BL">The instance of BlackLightning to remove from the list.</param>
		private static void RemoveInstance(BlackLightning BL) {
			_InstancesMutex.WaitOne();
			_Instances.Remove(BL);
			int Count = _Instances.Count;
			_InstancesMutex.ReleaseMutex();
			
			// if there are no instances left, exit the application
			if (Count == 0) {
				_InstancesMutex.Close();
				SingletonApplication.Exit();
			}
		}

		// member data
		private SalFile        _SalFile        = null; // the SAL file used to login to the game
		private Dispatcher     _Dispatcher     = null; // the dispatcher that handles packet receiving and dispatching
		private PluginManager  _PluginManager  = null; // the plugin manager that handles all the plugins and modules
		private PacketRegistry _PacketRegistry = null; // the packet registry which handles all the filter registrations
		private Timekeeper     _Timekeeper     = null; // the timekeeper tracking time for us
		private OptionSet      _Options        = null; // Black Lightning's global options
		private ImageList      _Icons          = null; // an ImageList containing all our icons

		// member data - login information
		private string _Username = null;
		private string _Password = null;
		private string _GameCode = null;
		private string _CharCode = null;
		private string _FECode   = null;

		/// <summary>
		/// Creates a new BlackLightning instance.
		/// </summary>
		public BlackLightning() {
			// create our data
			_PacketRegistry = new PacketRegistry();
			_Timekeeper     = new Timekeeper();

			// load up our icons
			_Icons = new ImageList();
			_Icons.Images.Add(Resources.BlackLightningIcon);
			_Icons.Images.Add(Resources.BlackLightningFilesIcon);
			_Icons.Images.Add(Resources.BlackLightningFileIcon);
			_Icons.Images.Add(Resources.BlackLightningGearIcon);
			_Icons.Images.Add(Resources.BlackLightningKeysIcon);
			_Icons.Images.Add(Resources.BlackLightningKeyIcon);
			
			// load our options
			LoadOptions();
		}

		/// <summary>
		/// Gets the SAL file that was last used to login to the game.
		/// </summary>
		public SalFile SalFile {
			get {
				return _SalFile;
			}
		}

		/// <summary>
		/// Gets the Dispatcher that's handling networking for this BlackLightning instance.
		/// </summary>
		public Dispatcher Dispatcher {
			get {
				return _Dispatcher;
			}
		}

		/// <summary>
		/// Gets the PluginManager that's handling the plugins and modules for this BlackLightning instance.
		/// </summary>
		public PluginManager PluginManager {
			get {
				return _PluginManager;
			}
		}

		/// <summary>
		/// Gets the PacketRegistry which is handling all the filter registrations for this BlackLightning instance.
		/// </summary>
		public PacketRegistry PacketRegistry {
			get {
				return _PacketRegistry;
			}
		}

		/// <summary>
		/// Gets the Timekeeper which is tracking time for this BlackLightning instance.
		/// </summary>
		public Timekeeper Timekeeper {
			get {
				return _Timekeeper;
			}
		}

		/// <summary>
		/// Gets the options for this BlackLightning instance.
		/// </summary>
		public OptionSet Options {
			get {
				return _Options;
			}
		}

		/// <summary>
		/// Executes the login sequence, then runs the front end, connects to the game, and starts playing.
		/// </summary>
		/// <param name="Args">Command-line arguments passed to the program at startup.</param>
		public void Run(string[] Args) {
			// login to the login server
			_SalFile = Login(Args);
			if (_SalFile == null) {
				Stop();
				return;
			}

			// now that we know which character is being played, load character-specific options
			try {
				XmlFile In = new XmlFile(BlackLightning.PreferencesPath + _SalFile.Character + BlackLightning.OptionsFileExtension);
				_Options.Load(In.RootTag, BlackLightning.CharacterOptionValueIndex);
			} catch (FileNotFoundException) {
				// do nothing, if there wasn't a file then we'll just get default values later
			}

			// find the path to the launcher
			string LauncherPath = FindLauncherPath();
			if (LauncherPath == null) {
				Stop();
				return;
			}

			// create a front end and connect to it
			FrontEnd FE = FrontEnd.Create(_SalFile);
			FE.ExecuteAndConnect(LauncherPath);

			// create a connection to the game server
			Connection C = new Connection();
			C.Blocking = true;
			C.Connect(_SalFile.GameHost, _SalFile.GamePort);

			// have the front end handshake the connection and login
			FE.Handshake(C, true);

			// create a game server
			GameServer GS = GameServer.Create(_SalFile, C, true);

			// hand both the front end and the game server off to a new dispatcher
			_Dispatcher = new Dispatcher(this, FE, GS);

			// create our plugin manager, which will auto-load any relevent plugins
			// this needs to be done after the dispatcher is created so that plugins/modules can send messages
			// however, it needs to be done before running the dispatcher so that the plugins/modules receive the initial packets
			_PluginManager = new PluginManager(this);

			// run the dispatcher
			_Dispatcher.Run();
		}

		/// <summary>
		/// Stops the program and shuts it down.
		/// </summary>
		public void Stop() {
			// unload any remaining plugins
			if (_PluginManager != null) {
				_PluginManager.Unload();
			}

			// save out our options
			if (_SalFile != null) {
				SaveOptions(BlackLightning.CharacterOptionValueIndex, BlackLightning.PreferencesPath + _SalFile.Character + BlackLightning.OptionsFileExtension);
			}
			SaveOptions(BlackLightning.UserOptionValueIndex, BlackLightning.PreferencesPath + BlackLightning.ApplicationExecutable.Replace(".exe","") + BlackLightning.OptionsFileExtension);
			SaveOptions(BlackLightning.GlobalOptionValueIndex, BlackLightning.ApplicationPath + BlackLightning.ApplicationExecutable.Replace(".exe", "") + BlackLightning.OptionsFileExtension);

			// stop the timekeeper
			_Timekeeper.Dispose();

			// remove ourselves from the global list
			BlackLightning.RemoveInstance(this);
		}

		/// <summary>
		/// Disconnects from the game server, runs an automated login sequence, then re-connects and logs into the game.
		/// </summary>
		public void Reconnect() {
			// relogin to the login server
			_SalFile = Relogin();
			if (_SalFile == null) {
				_Dispatcher.SendPacket(new RawBLPacket(Program.BlackLightning, Program.FrontEnd, "Unable to re-login to the login server."));
				return;
			}

			// create a new connection to the game server
			Connection C = new Connection();
			C.Blocking = true;
			C.Connect(_SalFile.GameHost, _SalFile.GamePort);

			// have the front end handshake the connection and login
			_Dispatcher.FrontEnd.Handshake(C, false);

			// create a game server
			GameServer GS = GameServer.Create(_SalFile, C, false);

			// give the new game server to the dispatcher
			_Dispatcher.Reconnect(GS);
		}

		/// <summary>
		/// Displays an OptionDialog with the LoginClients options as the root.
		/// </summary>
		public void ShowLoginClientOptions() {
			// we'll need to assert that we have permission to read from the environment later
			EnvironmentPermission P = new EnvironmentPermission(PermissionState.Unrestricted);

			// setup our option dialog
			OptionDialog D     = new OptionDialog();
			D.RootOptionSet    = _Options;
			D.Icon             = Resources.BlackLightningIcon;
			D.ImageList        = _Icons;
			D.Sorter           = new OptionSetNameSorter();

			// assert that we can read from the environment, even though our caller cannot necessarily
			P.Assert();
			D.ValueIndexNames.Add(BlackLightning.GlobalOptionValueIndex, "Computer: " + Environment.MachineName);
			D.ValueIndexNames.Add(BlackLightning.UserOptionValueIndex,   "User: " + Environment.UserName);

			// revert the assert so the dialog can't do anything weird
			EnvironmentPermission.RevertAssert();

			// show the dialog
			D.ShowDialog();
		}

		/// <summary>
		/// Displays an OptionDialog with the Plugins options as the root.
		/// </summary>
		public void ShowPluginOptions() {
			// we'll need to assert that we have permission to read from the environment later
			EnvironmentPermission P = new EnvironmentPermission(PermissionState.Unrestricted);

			// setup our option dialog
			OptionDialog D  = new OptionDialog();
			D.RootOptionSet = _Options;
			D.Icon          = Resources.BlackLightningIcon;
			D.ImageList     = _Icons;
			D.Sorter        = new OptionSetNameSorter();
			
			// assert that we can read from the environment, even though our caller cannot necessarily
			P.Assert();
			D.ValueIndexNames.Add(BlackLightning.GlobalOptionValueIndex,    "Computer: " + Environment.MachineName);
			D.ValueIndexNames.Add(BlackLightning.UserOptionValueIndex,      "User: " + Environment.UserName);
			D.ValueIndexNames.Add(BlackLightning.CharacterOptionValueIndex, "Character: " + _SalFile.Character);
			
			// revert the assert so the dialog can't do anything weird
			EnvironmentPermission.RevertAssert();

			// show the dialog
			D.Shown        += OptionDialog_Shown;
			D.ShowDialog();

			// bring the front end back to the front after the dialog closes
			_Dispatcher.FrontEnd.BringToFront();
		}

		/// <summary>
		/// Called when one of our OptionDialogs is shown with the Plugin options in it.
		/// </summary>
		/// <param name="Sender">The Form that was shown.</param>
		/// <param name="e">No arguments.</param>
		private void OptionDialog_Shown(object Sender, EventArgs e) {
			// set the option dialog as the foreground window
			BlackLightning.SetForegroundWindow((Sender as Form).Handle);
		}

		/// <summary>
		/// Loads the Options files from disk and registers our program-wide options.
		/// </summary>
		private void LoadOptions() {
			// create our options
			_Options = new OptionSet(BlackLightning.BlackLightningOptionSetName);

			// load in the global options
			try {
				// load in the global and user options
				LoadOptions(_Options, BlackLightning.GlobalOptionValueIndex, BlackLightning.ApplicationPath + BlackLightning.ApplicationExecutable.Replace(".exe", "") + BlackLightning.OptionsFileExtension);
			} catch (FileNotFoundException) {
				_Options.AddOptionSet(new OptionSet(BlackLightning.LoginClientsOptionSetName));
			}

			// load in the user options
			try {
				LoadOptions(_Options, BlackLightning.UserOptionValueIndex,   BlackLightning.PreferencesPath + BlackLightning.ApplicationExecutable.Replace(".exe", "") + BlackLightning.OptionsFileExtension);
			} catch (FileNotFoundException) {
				_Options.AddOptionSet(new OptionSet(BlackLightning.PluginsOptionSetName));
			}

			// make sure we have a Modules and Login Clients section
			OptionSet PluginsOS      = _Options.GetOrCreateOptionSet(BlackLightning.PluginsOptionSetName);
			OptionSet LoginClientsOS = _Options.GetOrCreateOptionSet(BlackLightning.LoginClientsOptionSetName);

			// setup the option set icons
			_Options.IconIndex         = BlackLightning.IconIndex;
			_Options.SelectedIconIndex = BlackLightning.IconIndex;
			PluginsOS.IconIndex         = BlackLightning.FilesIconIndex;
			PluginsOS.SelectedIconIndex = BlackLightning.FilesIconIndex;
			LoginClientsOS.IconIndex         = BlackLightning.KeysIconIndex;
			LoginClientsOS.SelectedIconIndex = BlackLightning.KeysIconIndex;

			// register our program-wide options
			Option O = _Options.GetOrCreateOption(LauncherPathOptionName);
			O.Initialize(new OptionInitializer(LauncherPathOptionDefault.GetType(), LauncherPathOptionDefault, LauncherPathOptionDescription, LauncherPathOptionCategory, false, false, null, LauncherPathOptionTypeEditor, BlackLightning.GlobalOptionValueIndex));
			O = _Options.GetOrCreateOption(DefaultLoginClientOptionName);
			O.Initialize(new OptionInitializer(DefaultLoginClientOptionDefault.GetType(), DefaultLoginClientOptionDefault, DefaultLoginClientOptionDescription, DefaultLoginClientOptionCategory, BlackLightning.GlobalOptionValueIndex, BlackLightning.UserOptionValueIndex));
			O = _Options.GetOrCreateOption(RunNextToolOptionName);
			O.Initialize(new OptionInitializer(RunNextToolOptionDefault.GetType(), RunNextToolOptionDefault, RunNextToolOptionDescription, RunNextToolOptionCategory, BlackLightning.GlobalOptionValueIndex, BlackLightning.UserOptionValueIndex));
			O = _Options.GetOrCreateOption(NextToolOptionName);
			O.Initialize(new OptionInitializer(NextToolOptionDefault.GetType(), NextToolOptionDefault, NextToolOptionDescription, NextToolOptionCategory, false, false, null, NextToolOptionTypeEditor, BlackLightning.GlobalOptionValueIndex, BlackLightning.UserOptionValueIndex));
			O = _Options.GetOrCreateOption(LaunchFromSGEOptionName);
			O.Initialize(new OptionInitializer(LaunchFromSGEOptionDefault.GetType(), LaunchFromSGEOptionDefault, LaunchFromSGEOptionDescription, LaunchFromSGEOptionCategory, BlackLightning.GlobalOptionValueIndex));
			O.Value = AssociatedWithSGE();
			O.ApplyNewValues();
			O.ValueChanged += LaunchFromSGE_ValueChanged;
			O = _Options.GetOrCreateOption(BackupSGELaunchOptionName);
			O.Initialize(new OptionInitializer(LauncherPathOptionDefault.GetType(), _Options.GetOption(LauncherPathOptionName).Value, "", "", true, BlackLightning.GlobalOptionValueIndex));
			O = _Options.GetOrCreateOption(LaunchFromSALOptionName);
			O.Initialize(new OptionInitializer(LaunchFromSALOptionDefault.GetType(), LaunchFromSALOptionDefault, LaunchFromSALOptionDescription, LaunchFromSALOptionCategory, BlackLightning.GlobalOptionValueIndex));
			O.Value = AssociatedWithSAL();
			O.ApplyNewValues();
			O.ValueChanged += LaunchFromSAL_ValueChanged;
			O = _Options.GetOrCreateOption(BackupSALLaunchOptionName);
			O.Initialize(new OptionInitializer(LauncherPathOptionDefault.GetType(), "\"" + _Options.GetOption(LauncherPathOptionName).Value + "\" %1", "", "", true, BlackLightning.GlobalOptionValueIndex));
		}

		/// <summary>
		/// Loads an options file into a given OptionSet and saving the values under a given index.
		/// </summary>
		/// <param name="Options">The OptionSet to load the options file into.</param>
		/// <param name="ValueIndex">The Option's value index to save the loaded values under.</param>
		/// <param name="Filename">The filename to load the values from.</param>
		private void LoadOptions(OptionSet Options, int ValueIndex, string Filename) {
			XmlFile In = new XmlFile(Filename);
			Options.Load(In.RootTag, ValueIndex);
		}

		/// <summary>
		/// Saves the Option values at the given index to the file with the given name.
		/// </summary>
		/// <param name="ValueIndex">The index of Option values to save.</param>
		/// <param name="Filename">The filename of the file to save the values to.</param>
		private void SaveOptions(int ValueIndex, string Filename) {
			XmlTag Tag = _Options.Save(ValueIndex);
			if (Tag == null) {
				File.Delete(Filename);
			} else {
				XmlFile Out = new XmlFile(Tag);
				Out.Save(Filename);
			}
		}

		/// <summary>
		/// Locates the launcher path from the options or asks the user for it if necessary.
		/// </summary>
		/// <returns>The path to the Launcher, or null if it couldn't be found.</returns>
		private string FindLauncherPath() {
			// check to see if they'd rather run a tool than the launcher
			bool RunNext = (bool)_Options.GetOption(RunNextToolOptionName).Value;

			// read the path from the appropriate option
			string Path = null;
			if (RunNext) {
				Path = _Options.GetOption(NextToolOptionName).Value as string;
			} else {
				Path = _Options.GetOption(LauncherPathOptionName).Value as string;
			}

			// check if that file actually exists
			if (File.Exists(Path)) {
				return Path;
			}

			// if they were running a tool and it didn't exist, ask if they want to use the instead
			DialogResult DR = MessageBox.Show("Black Lightning was unable to load the next tool: " + Path + "\nWould you like to run the Simutronics Launcher instead?\n\nClick Yes to use the Launcher or No to exit Black Lightning.", "Load Error", MessageBoxButtons.YesNo, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1);
			if (DR == DialogResult.Yes) {
				// grab the launcher path from the options
				Path = _Options.GetOption(LauncherPathOptionName).Value as string;

				// check if the launcher actually exists
				if (File.Exists(Path)) {
					return Path;
				}
			} else {
				return null;
			}


			// the next tool and launcher were both no goes, ask the user where the launcher is
			LauncherDialog LD = new LauncherDialog();
			LD.LauncherPath = Path;
			if (LD.ShowDialog() != DialogResult.OK) {
				return null;
			}

			// check if that file actually exists
			if (File.Exists(LD.LauncherPath)) {
				// save the new path to our options
				Option O = _Options.GetOption(LauncherPathOptionName);
				O.Value = LD.LauncherPath;
				O.ApplyNewValue();
				return LD.LauncherPath;
			}

			// nothing else we can do
			return null;
		}

		/// <summary>
		/// Locates and runs a LoginClient to login to the game.
		/// </summary>
		/// <param name="Args">Command-line arguments used to locate and pass parameters to a LoginClient.</param>
		/// <returns>The SalFile returned from the LoginClient.</returns>
		private SalFile Login(string[] Args) {
			// find the default login client
			Option   DefaultLC = _Options.GetOption(DefaultLoginClientOptionName);
			string[] Split     = (DefaultLC.Value as string).Split(new string[] {Module.FullnameSeparator}, StringSplitOptions.RemoveEmptyEntries);

			// setup the defaults
			string   PluginName      = Split[0];
			string   LoginClientName = Split[1];
			string[] LoginArgs       = null;

			// if we have arguments, investigate them
			if (Args.Length > 0) {
				if (Args[0].EndsWith(".~xt")) {
					// argument is an SAL file saved out by SGE, load it up
					SalFile SAL = SalFile.Load(Args[0]);

					// look up the name of the character they logged in with to complete the SAL file
					SGESettings SGE = new SGESettings(SGESettings.FindFile());
					SAL.Character = SGE.Character;
					
					// store SGE's settings for use with @reconnect
					_Username = SGE.Username;
					_GameCode = SGE.Game;
					_CharCode = SGE.Character;
					_FECode   = SGE.FrontEnd;
					if (SGE.RememberPassword) {
						_Password = SGE.Password;
					}

					// return the SAL file
					return SAL;
				} else if (Args[0].EndsWith(".sal")) {
					// if the filename of an SAL file is the first argument, then we don't need a LoginClient
					return SalFile.Load(Args[0]);
				} else {
					// if the first argument isn't an SAL file, we'll assume it's a login client name
					int Index = Args[0].IndexOf(LoginClient.FullnameSeparator);
					if (Index < 0) {
						throw new ArgumentException("Cannot find a login client because the first argument isn't an SAL file and doesn't contain a fully qualified login client name of the form 'PluginName.LoginClientName'.");
					}
					PluginName      = Args[0].Substring(0, Index);
					LoginClientName = Args[0].Substring(Index + LoginClient.FullnameSeparator.Length);
				}
			}

			// trim the first argument off the list
			if (Args.Length >= 2) {
				LoginArgs = new string[Args.Length-1];
				for (int i=1; i<Args.Length; ++i) {
					LoginArgs[i-1] = Args[i];
				}
			} else {
				LoginArgs = new string[0];
			}

			// run the login client we found
			return Login(PluginName, LoginClientName, LoginArgs);

		}

		/// <summary>
		/// Runs the AutoLogin LoginClient passing our stored login information.
		/// </summary>
		/// <returns>A new SAL file from the login server.</returns>
		private SalFile Relogin() {
			List<string> Unknowns = new List<string>();
			if (_Username == null) {
				Unknowns.Add("Username");
			}
			if (_Password == null) {
				Unknowns.Add("Password");
			}
			if (_GameCode == null) {
				Unknowns.Add("Game Code");
			}
			if (_CharCode == null) {
				Unknowns.Add("Character Code");
			}
			if (_FECode == null) {
				Unknowns.Add("Front End Code");
			}
			if (Unknowns.Count > 0) {
				string UnknownsList = string.Join(", ", Unknowns.ToArray());
				_Dispatcher.SendPacket(new RawBLPacket(Program.BlackLightning, Program.FrontEnd, "Unable to use @reconnect because the following login information is unavailable: " + UnknownsList + ".\nThis can occur for three reasons:\n  1. You logged in via the play.net web site.\n  2. You logged in with SGE and didn't check the 'Remember my password' box.\n  3. You logged in with a login client that does not support sharing the above information with Black Lightning."));
				return null;
			}

			// setup the arguments
			string[] Args = new string[5];
			Args[0] = "username=" + _Username;
			Args[1] = "password=" + _Password;
			Args[2] = "gamecode=" + _GameCode;
			Args[3] = "charcode=" + _CharCode;
			Args[4] = "fecode="   + _FECode;

			// run the auto login client
			return Login(AutoLoginClientPlugin, AutoLoginClientType, Args);
		}

		/// <summary>
		/// Runs the login functionality found in the given login client from the given plugin.
		/// </summary>
		/// <param name="PluginName">The name of the plugin to load the login client from.</param>
		/// <param name="ClientName">The name of the login client to load.</param>
		/// <param name="Args">The arguments to pass to the login client.</param>
		/// <returns>The SAL file returned by the login client.</returns>
		private SalFile Login(string PluginName, string ClientName, string[] Args) {
			// load the login client we found
			LoginClient LC = null;
			try {
				LC = new LoginClient(PluginName);
				LC.SendMessage += MessageReceived;
			} catch (LoadException) {
				DialogResult DR = MessageBox.Show("Black Lightning was unable to load the login client: " + PluginName + Module.FullnameSeparator + ClientName + "\nWould you like to load BLSGE instead?\n\nClick Yes to use BLSGE or No to exit Black Lightning.", "Load Error", MessageBoxButtons.YesNo, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1);
				if (DR == DialogResult.Yes) {
					return Login(BLSGEPlugin, BLSGEType, Args);
				} else {
					return null;
				}
			}

			// find the appropriate option set
			OptionSet LoginClientsSet = _Options.GetOptionSet(BlackLightning.LoginClientsOptionSetName);
			OptionSet OS = null;
			if (LoginClientsSet.HasOptionSet(LC.Name)) {
				OS = LoginClientsSet.GetOptionSet(LC.Name);
			} else {
				OS = new OptionSet(LC.Name);
				LoginClientsSet.AddOptionSet(OS);
			}

			// initialize the client
			try {
				LC.Initialize(ClientName, OS);
			} catch (LoadException) {
				LoginClientsSet.RemoveOptionSet(OS.Name);
				DialogResult DR = MessageBox.Show("Black Lightning was unable to load the login client: " + PluginName + Module.FullnameSeparator + ClientName + "\nWould you like to load BLSGE instead?\n\nClick Yes to use BLSGE or No to exit Black Lightning.", "Load Error", MessageBoxButtons.YesNo, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1);
				if (DR == DialogResult.Yes) {
					return Login(BLSGEPlugin, BLSGEType, Args);
				} else {
					return null;
				}
			}

			// run the login functionality
			SalFile SAL = LC.Login(Args);

			// store the login information
			if (SAL != null) {
				_Username = LC.Username;
				_Password = LC.Password;
				_GameCode = LC.GameCode;
				_CharCode = LC.CharCode;
				_FECode   = LC.FECode;
			}

			// unload the login client and remove all of its timers
			LC.Unload();
			Timekeeper.Timers.RemoveAll((new Timer.OwnerMatches(LC)).Check);

			// return the SAL file
			return SAL;
		}

		/// <summary>
		/// Sets (or unsets) SGE to execute Black Lightning instead of the Launcher.
		/// </summary>
		/// <param name="Yes">True to set SGE to run BL, false to set SGE to run the Launcher again.</param>
		private void AssociateWithSGE(bool Yes) {
			// grab the currently backed up value
			Option BackupSGE = _Options.GetOption(BlackLightning.BackupSGELaunchOptionName);

			// decide whether we'll be writing out Black Lightning or the backed up value
			string Value = Yes ? Application.ExecutablePath : BackupSGE.Value as string;

			// store the new backup value
			if (Yes) {
				IniFile WinIni = new IniFile(Environment.GetEnvironmentVariable("SystemRoot") + Path.DirectorySeparatorChar + "win.ini");
				BackupSGE.Value = WinIni["Simutronics", "LauncherPath"];
			} else {
				BackupSGE.Value = BackupSGE.DefaultValue;
			}

			// write out the chosen value to win.ini
			WritePrivateProfileString("Simutronics", "LauncherPath", Value, "win.ini");
		}

		/// <summary>
		/// Checks whether or not SGE is setup to run Black Lightning instead of the Launcher.
		/// </summary>
		/// <returns>True if SGE will run Black Lightning, false if it will run the Launcher (or anything else).</returns>
		private bool AssociatedWithSGE() {
            // load win.ini
			IniFile In = new IniFile(Environment.GetEnvironmentVariable("SystemRoot") + Path.DirectorySeparatorChar + "win.ini");

            // find the launcher path that SGE stores there
            string SGEPath = In["Simutronics", "LauncherPath"];
            if (SGEPath == null) {
                return false;
            }

            // check if it's the path to Black Lightning or not
			return SGEPath.ToLower() == Application.ExecutablePath.ToLower();
		}

		/// <summary>
		/// Sets (or unsets) Windows to execute Black Lightning instead of the Launcher for SAL files.
		/// </summary>
		/// <param name="Yes">True to set Windows to run BL, false to set Windows to run the Launcher again.</param>
		private void AssociateWithSAL(bool Yes) {
			// grab the appropriate registry key
			RegistryKey Key = Registry.LocalMachine.OpenSubKey(@"Software\Classes\Simutronics.Autolaunch\Shell\Open\command", true);

			// grab the currently backed up value
			Option BackupSAL = _Options.GetOption(BlackLightning.BackupSALLaunchOptionName);

			// decide whether we'll be writing out Black Lightning or the backed up value
			string Value = Yes ? "\"" + Application.ExecutablePath + "\" %1" : BackupSAL.Value as string;

			// store the new backup value
			BackupSAL.Value = Yes ? Key.GetValue(null) : BackupSAL.DefaultValue as string;

			// write out the chosen value to the registry
			Key.SetValue(null, Value);
		}

		/// <summary>
		/// Checks whether or not Black Lightning is associated with SAL files in Windows.
		/// </summary>
		/// <returns>True if Black Lightning is associated with SAL files in Windows, false if it is not.</returns>
		private bool AssociatedWithSAL() {
			// grab the appropriate registry key
			RegistryKey Key = Registry.LocalMachine.OpenSubKey(@"Software\Classes\Simutronics.Autolaunch\Shell\Open\command", true);
			return ( (Key.GetValue(null) as string).ToLower() == "\"" + Application.ExecutablePath.ToLower() + "\" %1" );
		}

		/// <summary>
		/// Called whenever we receive a message from our LoginClient.
		/// </summary>
		/// <param name="Sender">The LoginClient sending a message.</param>
		/// <param name="e">Arguments about the message sent.</param>
		private void MessageReceived(object Sender, SendMessageEventArgs e) {
			if (e.Message is ShowOptionsMessage) {
				this.ShowLoginClientOptions();
			} else if (e.Message is RegisterTimerMessage) {
				RegisterTimerMessage Msg = e.Message as RegisterTimerMessage;
				Timekeeper.Timers.Add(new Timer(Sender as IMessageHandler, Msg.ID, Msg.Start, Msg.ExpireCount, Msg.Interval));
			} else if (e.Message is UnregisterTimerMessage) {
				UnregisterTimerMessage Msg = e.Message as UnregisterTimerMessage;
				Timekeeper.Timers.RemoveAll((new Timer.IDMatches(Sender as IMessageHandler, Msg.ID)).Check);
			}
		}

		/// <summary>
		/// Called when the value of the LaunchFromSGE option changes.
		/// </summary>
		/// <param name="Sender">The Option whose value changed.</param>
		/// <param name="e">Arguments about the value that changed.</param>
		private void LaunchFromSGE_ValueChanged(object Sender, OptionValueChangedEventArgs e) {
			AssociateWithSGE((bool)e.NewValue);
		}
		
		/// <summary>
		/// Called when the value of the LaunchFromSAL option changes.
		/// </summary>
		/// <param name="Sender">The Option whose value changed.</param>
		/// <param name="e">Arguments about the value that changed.</param>
		private void LaunchFromSAL_ValueChanged(object Sender, OptionValueChangedEventArgs e) {
			AssociateWithSAL((bool)e.NewValue);
		}
	}
}
